﻿/* Copyright 2007-2008 dnAnalytics Project.
 *
 * Contributors to this file:
 * Jurgen Van Gael
 *
 * This file is part of dnAnalytics.  dnAnalytics is licensed under the 
 * Microsoft Public License. See License.txt for a complete copy of the
 * license.
 */

using System;
using dnAnalytics.Properties;

namespace dnAnalytics.Statistics.Distributions
{
    /// <summary>
    /// The binomial distribution is a distribution over integers parameterized by a count n and a probability p. It can be
    /// interpreted as the number of heads when drawing n coins with probability of head being p.
    /// </summary>
    /// <remarks>The distribution will use the <see cref="System.Random"/> by default. 
    /// Users can set the random number generator by using the <see cref="RandomNumberGenerator"/> property.
    /// 
    /// The statistics classes will check all the incoming parameters whether they are in the allowed
    /// range. This might involve heavy computation. Optionally, by setting Control.CheckDistributionParameters
    /// to false, all parameter checks can be turned off.</remarks>
    public class Binomial : IDiscreteDistribution
    {
        // The probability of a head.
        private readonly double mP;

        // The number of coins tossed.
        private readonly int mN;

        /// <summary>
        /// Construct a new Binomial distribution.
        /// </summary>
        /// <param name="p">The probability of a heads.</param>
        /// <param name="n">The number of coin flips.</param>
        /// <exception cref="ArgumentOutOfRangeException">If p is not in [0,1] and n is not > 0.</exception>
        public Binomial(double p, int n)
        {
            if (Control.CheckDistributionParameters)
            {
                CheckParameters(p,n);
            }
            
            mP = p;
            mN = n;
            RandomNumberGenerator = new System.Random();
        }

        #region IDistribution Members

        /// <summary>
        /// The mean of the distribution.
        /// </summary>
        public double Mean
        {
            get { return mP * mN; }
        }

        /// <summary>
        /// The standard deviation of the distribution.
        /// </summary>
        public double StdDev
        {
            get { return System.Math.Sqrt(mP * (1.0 - mP) * mN); }
        }

        /// <summary>
        /// The variance of the distribution.
        /// </summary>
        public double Variance
        {
            get { return mP * (1.0 - mP) * mN; }
        }

        /// <summary>
        /// The entropy of the distribution.
        /// </summary>
        public double Entropy
        {
            get
            {
                double E = 0.0;
                for (int k = 0; k <= mN; k++)
                {
                    E += Probability(k)
                         * Math.SpecialFunctions.BinomialLn(mN, k)
                         * (k * System.Math.Log(mP) + (mN - k) * System.Math.Log(1.0 - mP));
                }

                return E;
            }
        }

        /// <summary>
        /// Gets or sets the random number generator.
        /// </summary>
        /// <value>The random number generator used to generate a random sample.</value>
        public System.Random RandomNumberGenerator { get; set; }

        #endregion

        #region IDiscreteDistribution Members

        /// <summary>
        /// The mode of the distribution.
        /// </summary>
        public int Mode
        {
            get { return (int) System.Math.Floor((mN + 1) * mP); }
        }

        /// <summary>
        /// The median of the distribution.
        /// </summary>
        public int Median
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Computes the probability of a specific value.
        /// </summary>
        public double Probability(int val)
        {
            if (val >= 0 && val <= mN)
            {
                return Math.SpecialFunctions.Binomial(mN, val) * System.Math.Pow(mP, val) * System.Math.Pow(1.0 - mP, mN - val);
            }

            return 0.0;
        }

        /// <summary>
        /// Samples a Binomial distributed random variable.
        /// </summary>
        public int Sample()
        {
            return DoSample(RandomNumberGenerator, mP, mN);
        }

        /// <summary>
        /// Samples an array of Binomial distributed random variables.
        /// </summary>
        /// <param name="n">The number of variables needed.</param>
        public int[] Sample(int n)
        {
            return DoSample(RandomNumberGenerator, n, mP, mN);
        }

        #endregion

        private static void CheckParameters(double p, int n)
        {
            if (p < 0.0 || p > 1.0)
            {
                throw new ArgumentOutOfRangeException("p", Resources.ZeroOneRange);
            }
            if (n <= 0)
            {
                throw new ArgumentOutOfRangeException("n", Resources.NotPositive);
            }
        }

        /// <summary>
        /// Samples a Binomial distributed random variable.
        /// </summary>
        /// <param name="rnd">The random number generator to use.</param>
        /// <param name="p">The probability of generating heads.</param>
        /// <param name="n">The number of coins to flip.</param>
        public static int Sample(System.Random rnd, double p, int n)
        {
            if (Control.CheckDistributionParameters)
            {
                CheckParameters(p,n);
            }
            
            return DoSample(rnd, p, n);
        }

        /// <summary>
        /// Samples an array of Bernoulli distributed random variables.
        /// </summary>
        /// <param name="rnd">The random number generator to use.</param>
        /// <param name="m">The number of variables needed.</param>
        /// <param name="p">The probability of generating heads.</param>
        /// <param name="n">The number of coins to flip.</param>
        public static int[] Sample(System.Random rnd, int m, double p, int n)
        {
            if (Control.CheckDistributionParameters)
            {
                CheckParameters(p,n);
            }
            
            return DoSample(rnd, m, p, n);
        }

        private static int DoSample(System.Random rnd, double p, int n)
        {
            int k = 0;
            for (int i = 0; i < n; i++)
            {
                k += rnd.NextDouble() < p ? 1 : 0;
            }

            return k;
        }

        private static int[] DoSample(System.Random rnd, int m, double p, int n)
        {
            int[] ret = new int[m];
            for (int i = 0; i < ret.Length; i++)
            {
                ret[i] = DoSample(rnd, p, n);
            }
            return ret;
        }
    }
}